/*
 * Copyright (C) 2015 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.tencent.matrix.resource.analyzer.model;

import java.io.Serializable;
import java.util.LinkedHashMap;
import java.util.Map;

import static com.tencent.matrix.resource.common.utils.Preconditions.checkNotNull;
import static java.util.Collections.unmodifiableMap;

/**
 * Prevents specific references from being taken into account when computing the shortest strong
 * reference path from a suspected leaking instance to the GC roots.
 *
 * This class lets you ignore known memory leaks that you known about. If the shortest path
 * matches {@link ExcludedRefs}, than the heap analyzer should look for a longer path with nothing
 * matching in {@link ExcludedRefs}.
 */
public class ExcludedRefs implements Serializable {

    public static Builder builder() {
        return new BuilderWithParams();
    }

    public final Map<String, Map<String, Exclusion>> fieldNameByClassName;
    public final Map<String, Map<String, Exclusion>> staticFieldNameByClassName;
    public final Map<String, Exclusion> threadNames;
    public final Map<String, Exclusion> classNames;

    ExcludedRefs(BuilderWithParams builder) {
        this.fieldNameByClassName = unmodifiableRefStringMap(builder.fieldNameByClassName);
        this.staticFieldNameByClassName = unmodifiableRefStringMap(builder.staticFieldNameByClassName);
        this.threadNames = unmodifiableRefMap(builder.threadNames);
        this.classNames = unmodifiableRefMap(builder.classNames);
    }

    private Map<String, Map<String, Exclusion>> unmodifiableRefStringMap(
            Map<String, Map<String, ParamsBuilder>> mapmap) {
        LinkedHashMap<String, Map<String, Exclusion>> fieldNameByClassName = new LinkedHashMap<>();
        for (Map.Entry<String, Map<String, ParamsBuilder>> entry : mapmap.entrySet()) {
            fieldNameByClassName.put(entry.getKey(), unmodifiableRefMap(entry.getValue()));
        }
        return unmodifiableMap(fieldNameByClassName);
    }

    private Map<String, Exclusion> unmodifiableRefMap(Map<String, ParamsBuilder> fieldBuilderMap) {
        Map<String, Exclusion> fieldMap = new LinkedHashMap<>();
        for (Map.Entry<String, ParamsBuilder> fieldEntry : fieldBuilderMap.entrySet()) {
            fieldMap.put(fieldEntry.getKey(), new Exclusion(fieldEntry.getValue()));
        }
        return unmodifiableMap(fieldMap);
    }

    @Override
    public String toString() {
        String string = "";
        for (Map.Entry<String, Map<String, Exclusion>> classes : fieldNameByClassName.entrySet()) {
            String clazz = classes.getKey();
            for (Map.Entry<String, Exclusion> field : classes.getValue().entrySet()) {
                String always = field.getValue().alwaysExclude ? " (always)" : "";
                string += "| Field: " + clazz + "." + field.getKey() + always + "\n";
            }
        }
        for (Map.Entry<String, Map<String, Exclusion>> classes : staticFieldNameByClassName.entrySet()) {
            String clazz = classes.getKey();
            for (Map.Entry<String, Exclusion> field : classes.getValue().entrySet()) {
                String always = field.getValue().alwaysExclude ? " (always)" : "";
                string += "| Static field: " + clazz + "." + field.getKey() + always + "\n";
            }
        }
        for (Map.Entry<String, Exclusion> thread : threadNames.entrySet()) {
            String always = thread.getValue().alwaysExclude ? " (always)" : "";
            string += "| Thread:" + thread.getKey() + always + "\n";
        }
        for (Map.Entry<String, Exclusion> clazz : classNames.entrySet()) {
            String always = clazz.getValue().alwaysExclude ? " (always)" : "";
            string += "| Class:" + clazz.getKey() + always + "\n";
        }
        return string;
    }

    static final class ParamsBuilder {
        String name;
        String reason;
        boolean alwaysExclude;
        final String matching;

        ParamsBuilder(String matching) {
            this.matching = matching;
        }
    }

    public interface Builder {
        BuilderWithParams instanceField(String className, String fieldName);

        BuilderWithParams staticField(String className, String fieldName);

        BuilderWithParams thread(String threadName);

        BuilderWithParams clazz(String className);

        ExcludedRefs build();
    }

    static class BuilderWithParams implements Builder {

        private final Map<String, Map<String, ParamsBuilder>> fieldNameByClassName =
                new LinkedHashMap<>();
        private final Map<String, Map<String, ParamsBuilder>> staticFieldNameByClassName =
                new LinkedHashMap<>();
        private final Map<String, ParamsBuilder> threadNames = new LinkedHashMap<>();
        private final Map<String, ParamsBuilder> classNames = new LinkedHashMap<>();

        private ParamsBuilder lastParams;

        @Override
        public BuilderWithParams instanceField(String className, String fieldName) {
            checkNotNull(className, "mClassName");
            checkNotNull(fieldName, "fieldName");
            Map<String, ParamsBuilder> excludedFields = fieldNameByClassName.get(className);
            if (excludedFields == null) {
                excludedFields = new LinkedHashMap<>();
                fieldNameByClassName.put(className, excludedFields);
            }
            lastParams = new ParamsBuilder("field " + className + "#" + fieldName);
            excludedFields.put(fieldName, lastParams);
            return this;
        }

        @Override
        public BuilderWithParams staticField(String className, String fieldName) {
            checkNotNull(className, "mClassName");
            checkNotNull(fieldName, "fieldName");
            Map<String, ParamsBuilder> excludedFields = staticFieldNameByClassName.get(className);
            if (excludedFields == null) {
                excludedFields = new LinkedHashMap<>();
                staticFieldNameByClassName.put(className, excludedFields);
            }
            lastParams = new ParamsBuilder("static field " + className + "#" + fieldName);
            excludedFields.put(fieldName, lastParams);
            return this;
        }

        @Override
        public BuilderWithParams thread(String threadName) {
            checkNotNull(threadName, "threadName");
            lastParams = new ParamsBuilder("any threads named " + threadName);
            threadNames.put(threadName, lastParams);
            return this;
        }

        /**
         * Ignores all fields and static fields of all subclasses of the provided class name.
         */
        @Override
        public BuilderWithParams clazz(String className) {
            checkNotNull(className, "mClassName");
            lastParams = new ParamsBuilder("any subclass of " + className);
            classNames.put(className, lastParams);
            return this;
        }

        public BuilderWithParams named(String name) {
            lastParams.name = name;
            return this;
        }

        public BuilderWithParams reason(String reason) {
            lastParams.reason = reason;
            return this;
        }

        public BuilderWithParams alwaysExclude() {
            lastParams.alwaysExclude = true;
            return this;
        }

        @Override
        public ExcludedRefs build() {
            return new ExcludedRefs(this);
        }
    }
}
